﻿/*
   Copyright 2013 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Threading;
using Flower.Logging;
using Flower.Services;
using Flower.Services.Data;
using Flower.Services.Clients;
using System.Transactions;

namespace Flower.Security
{
    public class AuthorizationProvider : IAuthorizationProvider, IDisposable
    {
        private struct RoleContainer
        {
            public string FullName;
            public Role Role;
            public RoleContainer[] NestedRoles;
        }

        private static readonly Log Log = LogManager.CreateLog(typeof(AuthorizationProvider).Name);

        private static readonly int RolesPathPrefix = ("/Roles/").Length;

        private bool _isDisposed;
        private Timer _rolesRefreshTimer;
        private readonly TimeSpan _rolesRefreshInterval;
        private RoleContainer _roles;
        private bool _rolesLoadingFailure;
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

        private IDirectory _directory;

        public IDirectory Directory
        {
            set
            {
                if (_directory != null) throw new InvalidOperationException("Directory is already set for this instance.");
                if (value == null) throw new ArgumentNullException();

                _directory = value;
                OnRolesRefresh(null);
                _rolesRefreshTimer = new Timer(OnRolesRefresh, null, _rolesRefreshInterval, _rolesRefreshInterval);
            }
        }

        public string[] GetUserRoles(IIdentity identity)
        {
            string[] overridenRoles = AuthorizationOverride.Roles;

            if (overridenRoles != null)
            {
                return overridenRoles;
            }

            if (!identity.IsAuthenticated)
            {
                Log.Trace(() => string.Format("Identity {0} is not authenticated. Returning no roles.", identity.Name));
                return new string[0];
            }

            if (_rolesLoadingFailure)
            {
                throw new FaultException<DirectoryFault>
                (
                    new DirectoryFault 
                    {
                        Code = DirectoryFaultCode.AccessDenied
                    },
                    "Authorization failed."
                );
            }

            _lock.EnterReadLock();
            try
            {
                return GetUserRoles(identity.Name, _roles, new List<string>()).ToArray();
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        public AuthorizationProvider(TimeSpan rolesRefreshInterval)
        {
            _rolesRefreshInterval = rolesRefreshInterval;
        }

        private void OnRolesRefresh(object arg)
        {
            Log.Trace(() => "Refreshing roles.");

            try
            {
                var roles = new RoleContainer();
                bool failed;

                try
                {
                    roles = LoadRoles(new Role(), "/Roles");
                    failed = false;
                }
                catch (AggregateException ex)
                {
                    Exception firstException = ex.InnerExceptions.First();

                    if 
                    (
                        (firstException is WcfClientException) &&
                        !(firstException.InnerException is FaultException)
                    )
                    {
                        Log.Warning("Failed to update roles due to a directory communication error.");
                        return;
                    }

                    Log.Error("Failed to load roles.", ex);
                    failed = true;
                }
                catch (Exception ex)
                {
                    AggregateException aggEx = ex as AggregateException;

                    if (aggEx != null)
                    {
                        ex = aggEx.InnerExceptions.First();
                    }

                    if
                    (
                        (ex is WcfClientException) &&
                        !(ex.InnerException is FaultException)
                    )
                    {
                        Log.Warning("Failed to update roles due to a directory communication error.");
                        return;
                    }


                    Log.Error("Failed to load roles.", ex);
                    failed = true;
                }

                _lock.EnterWriteLock();
                try
                {
                    _rolesLoadingFailure = failed;

                    if (!failed)
                    {
                        _roles = roles;
                    }
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }
            catch (Exception ex)
            {
                Log.Error("Unexpected error on roles refresh.", ex);
            }
        }

        public void RefreshRoles()
        {
            Task.Factory.StartNew(() => OnRolesRefresh(null));
        }

        private static List<string> GetUserRoles(string userName, RoleContainer container, List<string> roles)
        {
            if (container.Role.Users.Any(u => u.Equals(userName, StringComparison.OrdinalIgnoreCase)))
            {
                roles.Add(container.FullName);
                return roles;
            }

            foreach (RoleContainer nestedRole in container.NestedRoles)
            {
                GetUserRoles
                (
                    userName, 
                    nestedRole,
                    roles
                );
            }

            Log.Trace(() => string.Format("The following roles found for the identity {0}: {1}.", userName, string.Join(", ", roles)));

            return roles;
        }

        private RoleContainer LoadRoles(Role role, string basePath, IProducerConsumerCollection<Exception> exceptions = null)
        {
            bool throwHere;

            if (exceptions == null)
            {
                exceptions = new ConcurrentBag<Exception>();
                throwHere = true;
            }
            else
            {
                throwHere = false;
            }

            IList<DirectoryEntry> roles;

            using(new AuthorizationOverride("Administrator"))
            {
                roles = TransactionHelper.Scope
                (
                    IsolationLevel.ReadCommitted, 
                    TimeSpan.FromSeconds(30), 
                    () =>
                        _directory.GetChildren
                        (
                            basePath,
                            DirectoryEntryTypes.Role,
                            SortProperty.Default,
                            SortOrder.Asc,
                            0,
                            long.MaxValue
                        )
                );
            }

            var nestedRoles = new RoleContainer[roles.Count];

            if (nestedRoles.Length > 0)
            {
                Parallel.For
                (
                    0,
                    nestedRoles.Length,
                    i => 
                    {
                        nestedRoles[i] = 
                            LoadRoles((Role)roles[i], string.Concat(basePath, "/", roles[i].Name), exceptions);
                    }
                );
            }

            if (throwHere && exceptions.Count > 0)
            {
                throw new AggregateException(exceptions);
            }

            return new RoleContainer
            {
                FullName = (basePath.Length > RolesPathPrefix) ? basePath.Substring(RolesPathPrefix) : string.Empty,
                Role = role,
                NestedRoles = nestedRoles
            };
        }

        public void Dispose()
        {
            if (!_isDisposed)
            {
                _rolesRefreshTimer.Dispose();

                _isDisposed = true;
            }

            GC.SuppressFinalize(this);
        }
    }
}
